
/*
 * Copyright (C) 2010 Bitglue. All rights reserved.
 *
 * $Id: ClasspathController.java bself $
 */

package com.bitglue.web.admin.webmvc;

import com.bitglue.web.admin.util.TransformUtils;
import com.bitglue.web.admin.util.XmlTreeUtils;
import com.bitglue.web.admin.webmvc.model.ClasspathModel;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.JarInputStream;
import java.util.zip.ZipEntry;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import org.w3c.dom.Element;

/**
 * ClasspathController for displaying Classpath info.
 * @author Bryan Patrick Self
 */
@Controller("classpathController")
@SessionAttributes(types = ClasspathModel.class)
public class ClasspathController {

    protected final Logger log = Logger.getLogger(getClass());

    @Autowired
    @Qualifier("bundleContext")
    private BundleContext bundleContext;

    @RequestMapping(value = "/classloader.do", method = RequestMethod.GET)
    public ModelAndView bundleSetup(HttpServletRequest req) {
        log.debug("creating new classpath model for client "
          + req.getRemoteAddr());
        ClasspathModel cpModel = new ClasspathModel();
        List<ClasspathModel> models = new LinkedList<ClasspathModel>();
        for (Bundle b : bundleContext.getBundles()) {
            ClasspathModel cm = new ClasspathModel();
            cm.setId(b.getBundleId());
            cm.setLocation(b.getLocation().replaceAll(".*/", ""));
            cm.setSymbolicName(b.getSymbolicName());
            cm.setVersion(b.getVersion().toString());
            models.add(cm);
        }
        cpModel.setModels(models);
        return new ModelAndView("classpath", "cpModel", cpModel);
    }

    @RequestMapping(value = "/getClasspath.do")
    public void getClasspath(@RequestParam("ID") long bundleId,
      HttpServletRequest req, HttpServletResponse resp) throws Exception {
        resp.setContentType("text/xml");
        log.debug("retrieving classpath entries for client "
          + req.getRemoteAddr());

        Element root = XmlTreeUtils.newTree();
        for (Bundle b : bundleContext.getBundles()) {
            if (b.getBundleId() == bundleId) {
                buildTree(b, root);
                break;
            }
        }
        TransformUtils.transform(root, resp.getOutputStream());
    }

    /**
     * buildTree() reads jar zip file entries and attempts to
     * load each class it encounters. On the first successful class
     * load, the corresponding ClassLoader is returned.
     */
    private void buildTree(Bundle b, Element root) throws Exception {
        int id = 1;
        Element rsc = null;
        boolean gotPkgs = false;
        URL url = new URL(b.getLocation());
        JarInputStream jis = new JarInputStream(url.openStream());
        ZipEntry ze = null;
        while ((ze = jis.getNextEntry()) != null) {
            String entry = ze.getName();
            if (entry.endsWith("/"))
                continue;
            if (entry.endsWith(".class")) {
                if (!gotPkgs) {
                    String nm = entry.replaceAll("/", "\\.")
                      .replaceAll("\\.class$", "");
                    int tmpId = searchClassLoaders(b, nm, root, id);
                    if (tmpId != -1) {
                        gotPkgs = true;
                        id = tmpId;
                    }
                }
                continue;
            }
            if (rsc == null)
                rsc = XmlTreeUtils.newLeaf(root, id++, "resources");
            XmlTreeUtils.newLeaf(rsc, id++, entry);
        }
    }

    /**
     * loadPkgs() extracts all the packages known to a bundle's
     * classloader. This is achieved by first loading a known class
     * that exists inside the bundle and then getting the bundle's
     * classloader from the class. Next, search the classloader and
     * it's super classes for getPackages(). Finally, set the
     * protected getPackages() method to be accessible and invoke it.
     */
    private int searchClassLoaders(Bundle b, String name,
      Element root, int id) throws Exception {
        ClassLoader cl = null;
        try {
            Class clazz = b.loadClass(name);
            cl = clazz.getClassLoader();
        } catch (ClassNotFoundException cnfe) {
            return -1;
        }

        Element elm = XmlTreeUtils.newLeaf(root, id++, "packages");
        do {
            elm = XmlTreeUtils.newLeaf(elm, id++, cl.toString());
            String[] pkgs = loadPkgs(cl);
            Arrays.sort(pkgs);
            for (String st : pkgs)
                XmlTreeUtils.newLeaf(elm, id++, st);
        } while ((cl = cl.getParent()) != null);
        return id;
    }

    private String[] loadPkgs(ClassLoader c) {
        Class<?> clz = c.getClass();
        do {
            try {
                Method m = clz.getDeclaredMethod("getPackages");
                m.setAccessible(true);
                Package[] parr = (Package[])m.invoke(c, new Object[] {});
                String[] ret = new String[parr.length];
                for (int x = 0; x < parr.length; x++)
                    ret[x] = new String(parr[x].getName());
                return ret;
            } catch (Exception ex) {
                continue;
            }
        } while ((clz = clz.getSuperclass()) != null);
        return null;
    }

}
